// Package main is a tool to automate the creation of code init function.
// Inspired by `github.com/golang/tools/cmd/stringer`.
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/constant"
	"go/format"
	"go/token"
	"go/types"
	"html/template"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"golang.org/x/tools/go/packages"
)

var errCodeDocPrefix = `# 错误码

！！系统错误码列表，由 {{.}}codegen -type=int -doc{{.}} 命令生成，不要对此文件做任何更改。

## 功能说明

如果返回结果中存在 {{.}}code{{.}} 字段，则表示调用 API 接口失败。例如：

{{.}}{{.}}{{.}}json
{
  "code": 100101,
  "message": "Database error"
}
{{.}}{{.}}{{.}}

上述返回中 {{.}}code{{.}} 表示错误码，{{.}}message{{.}} 表示该错误的具体信息。每个错误同时也对应一个 HTTP 状态码，比如上述错误码对应了 HTTP 状态码 500(Internal Server Error)。

## 错误码列表

系统支持的错误码列表如下：

| Identifier | Code | HTTP Code | Description |
| ---------- | ---- | --------- | ----------- |
`

var (
	// flag.String 返回 存储标志值字符串的指针
	// flag.StringVar 参数 p 是指向一个字符串存储标志值变量的指针
	typeNames  = flag.String("type", "", "comma-separated list of type names; must be set")
	output     = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
	trimprefix = flag.String("trimprefix", "", "将生成的内容中的 prefix 前缀删除")
	buildTags  = flag.String("tags", "", "应用的一组逗号分隔的构建标签")
	doc        = flag.Bool("doc", false, "如果为 true, 则仅以 markdown 格式生成错误代码文档")
)

// Usage is a replacement usage function for the flags package.
func Usage() {
	fmt.Fprintf(os.Stderr, "Usage of codegen:\n")
	fmt.Fprintf(os.Stderr, "\tcodegen [flags] -type T [directory]\n")
	fmt.Fprintf(os.Stderr, "\tcodegen [flags] -type T files... # Must be a single package\n")
	fmt.Fprintf(os.Stderr, "Flags:\n")
	flag.PrintDefaults() // 是一个函数，用于打印当前命令行标志的默认值。它遍历已注册的命令行标志，并为每个标志打印名称、默认值和用法信息
}

func main() {
	log.SetFlags(0)            // 标志设置为零意味着不使用任何标志，因此日志输出不包含日期、时间或其他信息。这通常用于在不需要其他元数据的情况下，只输出日志消息本身
	log.SetPrefix("codegen: ") // 打印日志的时候 打印的日志就会以 "codegen: " 开头
	flag.Usage = Usage         // flag.Usage 是一个函数类型的变量，它定义了如何输出在用户输入无效参数时应该显示的消息
	flag.Parse()               // 解析命令行参数

	//// 调试用
	//*typeNames = "int"
	//*doc = true
	//*output = "C:/Users/CZC/Desktop/go-mxshop/project/v2/mxshop/pkg/code/error_code_generated.md"

	if len(*typeNames) == 0 {
		flag.Usage()
		os.Exit(2)
	}
	types := strings.Split(*typeNames, ",")

	var tags []string
	if len(*buildTags) > 0 { // 只有携带了 tag 参数的文件才能编译运行	如 这样 // +build jsoniter
		tags = strings.Split(*buildTags, ",")
	}

	args := flag.Args()
	if len(args) == 0 {
		// Default: 在当前目录下执行
		args = []string{"."}
		//// 调试用
		//args = []string{"C:/Users/CZC/Desktop/go-mxshop_api/project/v2/mxshop_api/pkg/code"}
	}

	// 只需解析一次包
	var dir string
	g := Generator{
		trimPrefix: *trimprefix, // 设置去掉前缀
	}
	// TODO(suzmue): accept other patterns for packages (directories, list of files, import paths, etc).
	if len(args) == 1 && isDirectory(args[0]) { // 如果只有一个字符串 并且是目录
		dir = args[0]
	} else {
		if len(tags) != 0 {
			log.Fatal("-tags option applies only to directories, not when files are specified") // 退出运行
		}
		dir = filepath.Dir(args[0]) // 尝试 返回上层目录
	}

	g.parsePackage(args, tags) // args:目录  tags:应用的构建标记

	if !*doc { // 如果不需要错误文档
		// Print the header and package clause.
		g.Printf("// license that can be found in the LICENSE file.\n")
		g.Printf("\n")
		g.Printf("// Code generated by \"codegen %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
		g.Printf("\n")
		g.Printf("package %s", g.pkg.name)
		g.Printf("\n")
	}

	// 对于每个类型运行生成代码
	var src []byte // 输出流临时存放
	for _, typeName := range types {
		if *doc {
			g.generateDocs(typeName)
		} else {
			g.generate(typeName)

		}
	}

	// Format the output.
	src = g.format()

	// Write to file.
	outputName := *output // 写入的文件路径
	if outputName == "" { // 如果为空 则创建一个文件
		absDir, _ := filepath.Abs(dir)
		// strings.ReplaceAll:替换所有 	Base:返回path的最后一个元素
		baseName := fmt.Sprintf("%s_generated.go", strings.ReplaceAll(filepath.Base(absDir), "-", "_"))
		if len(flag.Args()) == 1 {
			baseName = fmt.Sprintf(
				"%s_generated.go",
				strings.ReplaceAll(filepath.Base(strings.TrimSuffix(flag.Args()[0], ".go")), "-", "_"),
			)
		}

		outputName = filepath.Join(dir, strings.ToLower(baseName))
	}
	err := os.WriteFile(outputName, src, 0600)
	if err != nil {
		log.Fatalf("writing output: %s", err)
	}
}

// isDirectory 报告命名文件是否为目录
func isDirectory(name string) bool {
	info, err := os.Stat(name)
	if err != nil {
		log.Fatal(err)
	}

	return info.IsDir() // 是否是 目录
}

// Generator 结构体用于保存代码分析状态。主要用于缓存输出结果以便通过 format.Source 进行格式化。
type Generator struct {
	buf bytes.Buffer // 积累输出 output.
	pkg *Package     // Package 扫描包

	trimPrefix string
}

// Printf like fmt.Printf, but add the string to g.buf.
// Printf 将格式化后的字符串写入 g.buf.
func (g *Generator) Printf(format string, args ...interface{}) {
	fmt.Fprintf(&g.buf, format, args...)
}

// File holds a single parsed file and associated data.
type File struct {
	pkg  *Package  // Package to which this file belongs.
	file *ast.File // Parsed AST.
	// These fields are reset for each type being generated.
	typeName string  // Name of the constant type.
	values   []Value // Accumulator for constant values of that type.

	trimPrefix string
}

// Package defines options for package.
type Package struct {
	name  string
	defs  map[*ast.Ident]types.Object
	files []*File
}

// parsePackage 函数分析由给定的模式和标记构造的单个包
// 如果出现错误，parsePackage 函数将终止程序
func (g *Generator) parsePackage(patterns []string, tags []string) {
	cfg := &packages.Config{
		// packages.NeedSyntax：在加载包的元数据的基础上，还会加载包的源码文件，并解析成语法树
		// packages.NeedTypes：在加载包的源码文件的基础上，还会对其进行类型检查，生成包的类型信息
		// packages.NeedTypesInfo | packages.NeedImports | packages.NeedDeps: 加载完整的类型信息，包名信息和所有直接/间接依赖的包信息
		Mode: packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypes | packages.NeedImports | packages.NeedDeps | packages.NeedName,
		// TODO: Need to think about constants in test files. Maybe write type_string_test.go
		// in a separate pass? For later.
		Tests:      false,                                                      // 是否加载测试文件  默认值为 false 不加载
		BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))}, //
	}
	pkgs, err := packages.Load(cfg, patterns...) // 函数返回的信息是一个 []*packages.Package 类型的切片，其中每个元素都表示一个被分析的包。每个 *packages.Package 类型的对象包含了对应包的语法树、类型信息、依赖关系等各种信息
	if err != nil {
		log.Fatal(err)
	}
	if len(pkgs) != 1 {
		log.Fatalf("error: %d packages found", len(pkgs))
	}
	g.addPackage(pkgs[0]) // 获取 当前 main.go 解析的代码内容
}

// addPackage 函数将一个类型检查后的 Package 和它的语法文件添加到生成器中
func (g *Generator) addPackage(pkg *packages.Package) {
	g.pkg = &Package{
		name:  pkg.Name,                       // 包名 code
		defs:  pkg.TypesInfo.Defs,             // pkg.TypesInfo 包的类型信息
		files: make([]*File, len(pkg.Syntax)), // pkg.Syntax 包下的的语法树
	}

	for i, file := range pkg.Syntax { // 遍历此包下的语法树 一个文件生成一个语法树
		g.pkg.files[i] = &File{
			file:       file,         // 此语法树 所有内容 	指针
			pkg:        g.pkg,        // 此包 所有内容		指针
			trimPrefix: g.trimPrefix, // 需要去除的前缀
		}
	}
}

// generate 会为指定的类型生成注册函数的代码
func (g *Generator) generate(typeName string) {
	values := make([]Value, 0, 100)
	for _, file := range g.pkg.files { // 在 ast.File 上做了一层封装  遍历 pkg 下的所有 语法树
		log.Printf("find file %s %d", file.file.Name, file.file.Name.NamePos)
		// 设置本次 walker 的状态
		file.typeName = typeName
		file.values = nil
		if file.file != nil { // 判断是否有语法树
			ast.Inspect(file.file, file.genDecl)
			values = append(values, file.values...)
		}
	}

	if len(values) == 0 {
		log.Fatalf("no values defined for type %s", typeName)
	}
	// Generate code that will fail if the constants change value.
	g.Printf("\t// init register error codes defines in this source code to `czc/pkg/errors`\n")
	g.Printf("func init() {\n")
	for _, v := range values {
		code, description := v.ParseComment()
		g.Printf("\tregister(%s, %s, \"%s\")\n", v.originalName, code, description)
	}
	g.Printf("}\n")
}

// generateDocs 为指定类型生成错误代码标记文档
func (g *Generator) generateDocs(typeName string) {
	values := make([]Value, 0, 100)
	for _, file := range g.pkg.files {
		// Set the state for this run of the walker.
		file.typeName = typeName
		file.values = nil
		if file.file != nil {
			ast.Inspect(file.file, file.genDecl)
			values = append(values, file.values...)
		}
	}

	if len(values) == 0 {
		log.Fatalf("no values defined for type %s", typeName)
	}

	tmpl, _ := template.New("doc").Parse(errCodeDocPrefix)
	var buf bytes.Buffer
	_ = tmpl.Execute(&buf, "`")

	// Generate code that will fail if the constants change value.
	g.Printf(buf.String())
	for _, v := range values {
		code, description := v.ParseComment()
		// g.Printf("\tregister(%s, %s, \"%s\")\n", v.originalName, code, description)
		g.Printf("| %s | %d | %s | %s |\n", v.originalName, v.value, code, description)
	}
	g.Printf("\n")
}

// format returns the gofmt-ed contents of the Generator's buffer.
func (g *Generator) format() []byte {
	// 其作用是对 Go 语言的源代码进行格式化，并返回格式化后的源代码
	src, err := format.Source(g.buf.Bytes())
	if err != nil {
		// Should never happen, but can arise when developing this code.
		// The user can compile the output to see the error.
		log.Printf("warning: internal error: invalid Go generated: %s", err)
		log.Printf("warning: compile the package to analyze the error")

		return g.buf.Bytes()
	}

	return src
}

// Value represents a declared constant.
type Value struct {
	comment      string
	originalName string // The name of the constant.
	name         string // The name with trimmed prefix.
	// The value is stored as a bit pattern alone. The boolean tells us
	// whether to interpret it as an int64 or a uint64; the only place
	// this matters is when sorting.
	// Much of the time the str field is all we need; it is printed
	// by Value.String.
	value  uint64 // Will be converted to int64 when needed.
	signed bool   // Whether the constant is a signed type.
	str    string // The string representation given by the "go/constant" package.
}

func (v *Value) String() string {
	return v.str
}

// ParseComment parse comment to http code and error code description.
func (v *Value) ParseComment() (string, string) {
	reg := regexp.MustCompile(`\w\s*-\s*(\d{3})\s*:\s*([A-Z].*)\s*\.\n*`)
	fmt.Println(v.comment)
	if !reg.MatchString(v.comment) {
		log.Printf("constant '%s' have wrong comment format, register with 500 as default", v.originalName)

		return "500", "Internal server error"
	}

	groups := reg.FindStringSubmatch(v.comment)
	if len(groups) != 3 { // 整个 1个分组  (\d{3}) 1个分组  ([A-Z].*) 1个分组
		return "500", "Internal server error"
	}

	return groups[1], groups[2]
}

// nolint: gocognit
// genDecl processes one declaration clause.
func (f *File) genDecl(node ast.Node) bool {
	decl, ok := node.(*ast.GenDecl)     // 断言是 GenDecl
	if !ok || decl.Tok != token.CONST { // 并且判断是否是 const 类型
		// We only care about const declarations.
		return true
	}
	// The name of the type of the constants we are declaring.
	// Can change if this is a multi-element declaration.
	typ := ""
	// Loop over the elements of the declaration. Each element is a ValueSpec:
	// a list of names possibly followed by a type, possibly followed by values.
	// If the type and value are both missing, we carry down the type (and value,
	// but the "go/types" package takes care of that).
	for _, spec := range decl.Specs { // 遍历 decl 中的 元素  即 decl.Specs
		vspec, _ := spec.(*ast.ValueSpec) // 保证成功，因为这是CONST
		// 如果 元素类型为 空 且 有值的情况下
		if vspec.Type == nil && len(vspec.Values) > 0 {
			// 如果常量是无类型的，则跳过此值说明并重置记忆的类型
			// 例如 "X = 1"，没有类型但有一个值
			typ = ""

			// 如果这是一个简单的类型转换，就记住类型。
			// 如果这实际上是一个函数调用，我们也不会介意；因为限定符调用不会被匹配（这将是SelectorExpr而不是Ident），
			// 并且只有在不寻常的情况下才会出现一个看起来像类型转换的函数调用。
			ce, ok := vspec.Values[0].(*ast.CallExpr) // 断言这是一个函数调用表达式 类似 w = string(10)
			if !ok {
				continue
			}
			id, ok := ce.Fun.(*ast.Ident)
			if !ok {
				continue
			}
			typ = id.Name // 记忆 type 类型
		}
		// 在 元素类型 不为空
		if vspec.Type != nil {
			// "X T". 有一个类型。记住它
			ident, ok := vspec.Type.(*ast.Ident)
			if !ok {
				continue
			}
			typ = ident.Name
		}
		if typ != f.typeName {
			// 这不是我们要找的类型
			continue
		}
		// 我们现在有一个名称列表（来自源代码的一行），所有名称都使用所需的类型进行声明
		// 获取它们的名称和实际值，并将它们存储在 f.values 中
		for _, name := range vspec.Names {
			if name.Name == "_" {
				continue
			}
			// 这个技巧允许类型检查器为我们查找值。有点棘手：查找被命名的对象，找到它的 types.Const，并提取它的值。
			obj, ok := f.pkg.defs[name]
			if !ok {
				log.Fatalf("no value for constant %s", name)
			}
			info := obj.Type().Underlying().(*types.Basic).Info()
			if info&types.IsInteger == 0 {
				log.Fatalf("can't handle non-integer constant type %s", typ)
			}
			value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST.
			if value.Kind() != constant.Int {
				log.Fatalf("can't happen: constant is not an integer %s", name)
			}
			i64, isInt := constant.Int64Val(value)
			u64, isUint := constant.Uint64Val(value)
			if !isInt && !isUint {
				log.Fatalf("internal error: value of %s is not an integer: %s", name, value.String())
			}
			if !isInt {
				u64 = uint64(i64)
			}

			// 上面处理各种类型不匹配 等问题后 开始赋值
			v := Value{
				originalName: name.Name,                  // const 里 元素名
				value:        u64,                        // 值
				signed:       info&types.IsUnsigned == 0, // 判断 obj的基础类型 不等 types.IsUnsigned
				str:          value.String(),
			}
			if vspec.Doc != nil && vspec.Doc.Text() != "" { // 判断是否有注释 且 注释内容不为空字符串
				v.comment = vspec.Doc.Text()
			} else if c := vspec.Comment; c != nil && len(c.List) == 1 { // 判断是否有行注释
				v.comment = c.Text()
			}

			v.name = strings.TrimPrefix(v.originalName, f.trimPrefix) // 去除 字符串 前缀子字符串
			f.values = append(f.values, v)
		}
	}

	return false
}
